﻿
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using MySql.Data.MySqlClient;
using MySql.Data.Types;
using ResearchRepository.Models;
using System.IO;

namespace ResearchRepository
{
	public static partial class GLOBALS
	{
		// session variable names
		public const string rootFileSessionVar = "rootFile";
		public const string selectedPathSessionVar = "selectedPath";
		public const string selectedIndexSessionVar = "selectedIndex";
	}
	public partial class RRBrowse : System.Web.UI.Page
	{
////////
////////	Properties
////////
		// local aliases for session vars
		protected RRFile rootFile
		{
			get
			{
				return (RRFile) Session[GLOBALS.rootFileSessionVar];
			}
			set
			{
				Session[GLOBALS.rootFileSessionVar] = value;
			}
		}
		protected string selectedTreePath
		{
			get
			{
				return (string) Session[GLOBALS.selectedPathSessionVar];
			}
			set
			{
				Session[GLOBALS.selectedPathSessionVar] = value;
			}
		}
		protected int selectedGridIndex
		{
			get
			{
				return (int) Session[GLOBALS.selectedIndexSessionVar];
			}
			set
			{
				Session[GLOBALS.selectedIndexSessionVar] = value;
			}
		}
		protected string selectedFilePath
		{
			get
			{
				return selectedTreePath + '/' + selectedGridIndex;
			}
		}
////////
////////	Page Event Handlers
////////
		protected void Page_Load(object sender, EventArgs e)
		{
			Title = GLOBALS.masterTitlePrefix + " - Browse Files";
			try
			{
				// initialize rootFile
				if (rootFile == null)
				{
					rootFile = RRDatabaseManager.getFileData(RRFile.EmptyRoot,
																Session[GLOBALS.userObjSessionVar] == null ? RRUser.GuestUserID : ((RRUser) Session[GLOBALS.userObjSessionVar]).UserID,
																1);	// just get the first level of dirs directly under the root
				}
				fileBrowserGrid.ShowHeader = true;
				if (IsPostBack)
				{
					//TODO??
				}
				else
				{
					TreeNode rootNode = new TreeNode("/"/* (-1)"*/, "0");
					populateTree(rootNode, rootFile);
					fileBrowserTree.Nodes.Add(rootNode);
					fileBrowserTree.ExpandDepth = 1;
					//fileBrowserTree.FindNode("0").Select();
				}
				//TODO more testing
			}
			catch(Exception x)
			{
				Session[GLOBALS.rootFileSessionVar] = null;		// on error, force file tree to be refresh
			}
		}
////////
////////	Action Button Handlers
////////
		//
		// Dir Actions
		//
		// implemented
		public void uploadButtonClicked(object sender, EventArgs e)
		{
			UploadForm.Visible = true;
			NewFolderForm.Visible = false;
			PermissionsRequestForm.Visible = false;
			DeleteConfirmation.Visible = false;

			UploadFileStatusLabel.Visible = false;
		}
		// implemented
		public void newFolderButtonClicked(object sender, EventArgs e)
		{
			UploadForm.Visible = false;
			NewFolderForm.Visible = true;
			PermissionsRequestForm.Visible = false;
			DeleteConfirmation.Visible = false;

			NewFolderStatusLabel.Visible = false;
		}
		// implemented
		public void dirPermButtonClicked(object sender, EventArgs e)
		{
			UploadForm.Visible = false;
			NewFolderForm.Visible = false;
			PermissionsRequestForm.Visible = true;
			DeleteConfirmation.Visible = false;

			PermissionsRequestStatusLabel.Visible = false;
			SubmitFilePermissionsRequestButton.Visible = false;
			SubmitDirPermissionsRequestButton.Visible = true;
			UpdatePermissionRequestButtons(getFileForPath(selectedTreePath));
		}
		//
		// File Actions
		//
		// implemented
		public void downloadButtonClicked(object sender, EventArgs e)
		{
			try
			{
				RRFile oldFile = getFileForPath(selectedFilePath);
				RRFile newFile = RRDatabaseManager.getFileData(oldFile,
																Session[GLOBALS.userObjSessionVar] == null ? RRUser.GuestUserID : ((RRUser) Session[GLOBALS.userObjSessionVar]).UserID,
																0);
				if (newFile != null && !newFile.IsDir && newFile.EffectivePermission >= RRPermissionType.Read)
				{
					newFile.SuperDir = oldFile.SuperDir;					// point to superDir to get full FilePath
					Response.ContentType = "APPLICATION/OCTET-STREAM";
					Response.AppendHeader("Content-Disposition", "Attachment; Filename=\"" + newFile.FileName + "\"");
					Response.Flush();
					Response.WriteFile(GLOBALS.repoDataRootDir + newFile.FilePath);
					Response.Flush();
					Response.End();	// always causes an exception to be thrown because the page is still trying to execute,
										// but if End() isn't used, then the entire HTML file is sent with and appended to the the target file data
				}
			}
			catch (Exception x)
			{
			}
		}
		//TODO
		public void renameButtonClicked(object sender, EventArgs e)
		{
			//TODO
		}
		//TODO
		public void copyToButtonClicked(object sender, EventArgs e)
		{
			//TODO
		}
		// implemented
		public void deleteButtonClicked(object sender, EventArgs e)
		{
			UploadForm.Visible = false;
			NewFolderForm.Visible = false;
			PermissionsRequestForm.Visible = false;
			DeleteConfirmation.Visible = true;

			DeleteComfirmationLabel.Text = "Are you sure you want to delete '" + getFileForPath(selectedFilePath).FileName + "'?";
			DeleteStatusLabel.Visible = false;
		}
		//TODO
		public void moveToButtonClicked(object sender, EventArgs e)
		{
			//TODO
		}
		// implemented
		public void filePermButtonClicked(object sender, EventArgs e)
		{
			UploadForm.Visible = false;
			NewFolderForm.Visible = false;
			PermissionsRequestForm.Visible = true;
			DeleteConfirmation.Visible = false;
			
			PermissionsRequestStatusLabel.Visible = false;
			SubmitFilePermissionsRequestButton.Visible = true;
			SubmitDirPermissionsRequestButton.Visible = false;
			UpdatePermissionRequestButtons(getFileForPath(selectedFilePath));
		}
		// TODO
        public void searchButtonClicked(object sender, EventArgs e)
        {
            //TODO
        }
////////
////////	Tree Event Handlers
////////
		// exactly what you think it is
		public void populateTreeNode(object sender, TreeNodeEventArgs  e)
		{
			updateTreeNode(e.Node);
		}
		// refresh view nodes and show dir contents in file grid when selected in fileBrowserTree
		public void treeNodeSelected(object sender, EventArgs  e)
		{
			TreeNode node = ((TreeView) sender).SelectedNode;
			if (node.Expanded != true)
			{
				node.Expand();
			}
			// always refresh node when selected
				// note: on first expand, node will already have been populated by node.Expand(),
				// so this call will most likely have no effect, unless the DB was updated in the microsecond between calls
			updateTreeNode(node);
			
			// store tree path to use in gridRowCreated during databinding
			selectedTreePath = node.ValuePath;
			SelectedDirPathLabel.Text = "/" + getFileForPath(selectedTreePath).FilePath;
			RRFile dir = getFileForPath(selectedTreePath);
			
			// cancel all pending dir and file actions
			CancelUploadButton_Click(sender, e);
			CancelNewFolderButton_Click(sender, e);
			CancelDeleteButton_Click(sender, e);
			CancelPermissionsRequestButton_Click(sender, e);

			// update permissions-dependent folder actions
			uploadButton.Enabled = dir.EffectivePermission >= RRPermissionType.Write;
			newFolderButton.Enabled = dir.EffectivePermission >= RRPermissionType.Write;
			dirPermButton.Enabled = dir.EffectivePermission < RRPermissionType.Admin;

			// disable file actions until row is selected
			downloadButton.Enabled = false;	// can't download a folder (TODO: yet)
			//copyToButton.Enabled = false;
			//renameButton.Enabled = false;
			deleteButton.Enabled = false;
			//moveToButton.Enabled = false;
			filePermButton.Enabled = false;

			// deselect GridView
			fileBrowserGrid.SelectedIndex = -1;

			// update GridView
			fileBrowserGrid.DataSource = dir.SubFiles;
			fileBrowserGrid.DataBind();
		}
		// TODO??
		public void treeNodeExpanded(object sender, TreeNodeEventArgs e)
		{
			//TODO??
		}
////////
////////	Tree Functions
////////
		// recursivley populate a TreeNode with a tree rooted at an RRFile
			// existing content of dirNode is unchanged
			// thus, it is generally expected that this is only run with an unpopulated dirNode to avoid repeated data
			// assumes dir.isDir()==true (if not, a NullReferenceException will be thrown when trying to access dir.SubFiles.Count)
		protected void populateTree(TreeNode dirNode, RRFile dir)
		{
			for (int i = 0; i < dir.SubFiles.Count; i++)
			{
				// only add directories to TreeNode
				if (dir.SubFiles[i].IsDir)
				{
					TreeNode subNode = new TreeNode(dir.SubFiles[i].FileName /*+ " (" + dir.SubFiles[i].FileID + ")"*/, ""+i);	// fileID is for DEBUG
					// if the sub-dir is empty, then try populating later
					if (dir.SubFiles[i].SubFiles.Count == 0)
					{
						subNode.PopulateOnDemand = true;
					}
					else // recursively populate now
					{
						populateTree(subNode, dir.SubFiles[i]);
					}
					dirNode.ChildNodes.Add(subNode);
				}
			}
		}
		// update a single node by confirming its existance and refreshing first-level sub-files
			// if the node's dir no longer exists, then it is removed from the file tree model and the fileBrowserTree view
			// when removing a sub-directory, the entire sub-tree (rooted at the sub-dir) is removed
			// when adding a sub-directory, only the sub-dir's level-0 is loaded (ie: the dir itself with no contents)
			// when updating a sub-directory, all existing level-2+ files (ie: sub-sub-files, sub-sub-sub-files, etc.) are retained but not updated
		public void updateTreeNode(TreeNode node)
		{
			// find old dir entry in tree hierarchy
			RRFile oldDir = getFileForPath(node.ValuePath);
			try
			{
				// fetch up-to-date data from DB; only gets the target dir and its level-1 sub-files
				RRFile newDir = RRDatabaseManager.getFileData(oldDir,
																Session[GLOBALS.userObjSessionVar] == null ? RRUser.GuestUserID : ((RRUser) Session[GLOBALS.userObjSessionVar]).UserID,
																1);
				// if dir is no longer accessible, then alert user and remove node from view
				if (newDir == null)
				{
					//TODO alert user
					node.Parent.ChildNodes.Remove(node);
				}
				// if dir has not changed, then do nothing
				else if (oldDir.Equals(newDir, 1))
				{
					return;
				}
				else // update dir data
				{
					// replace oldDir with newDir in file tree
					if (oldDir.SuperDir != null)	// if not root, then insert into tree
					{
						// keep new file data in same position as old data (must insert before remove)
						oldDir.SuperDir.SubFiles.Insert(oldDir.SuperDir.SubFiles.IndexOf(oldDir), newDir);
						// link newDir to same superDir
						newDir.SuperDir = oldDir.SuperDir;
					}
					else	// if root, then update root
					{
						rootFile = newDir;
					}

					// add/remove level-1 sub-nodes in view and copy level-2 sub-files in model
					int /*oldSubFileIndex = 0,*/ newSubFileIndex = 0, subNodeIndex = 0;
					//for (; oldSubFileIndex < oldDir.SubFiles.Count; oldSubFileIndex++)
					foreach (RRFile oldSubFile in oldDir.SubFiles)
					{
						// skip non-directory files
						//if (!oldDir.SubFiles[oldSubFileIndex].IsDir)
						if (!oldSubFile.IsDir)
						{
							continue;
						}
						// find new position of old sub-dir in new sub-dir list
						int newPos;
						for (newPos = newSubFileIndex;
								//newPos < newDir.SubFiles.Count && !newDir.SubFiles[newPos].Equals(oldDir.SubFiles[oldSubFileIndex], 0);
								newPos < newDir.SubFiles.Count && !newDir.SubFiles[newPos].Equals(oldSubFile, 0);
								newPos++)
						{}
						// if not found, then remove from node (view)
						if (newPos == newDir.SubFiles.Count)
						{
							node.ChildNodes.RemoveAt(subNodeIndex);
							continue;
						}
						// else (found): insert a new sub-node for each new sub-dir that comes before the old sub-dir's new position
						for (; newSubFileIndex < newPos; newSubFileIndex++)
						{
							// skip non-directory files
							if (!newDir.SubFiles[newSubFileIndex].IsDir)
							{
								continue;
							}
							TreeNode newSub = new TreeNode(newDir.SubFiles[newSubFileIndex].FileName /*+ " (" + newDir.SubFiles[newSubFileIndex].FileID + ")"*/, // fileID is for DEBUG
															"" + subNodeIndex);
							newSub.PopulateOnDemand = true;
							node.ChildNodes.AddAt(subNodeIndex, newSub);
							// advance to keep subNodeIndex at the current old sub-node
							subNodeIndex++;
						}
						// copy level-2 sub-files to new sub-dir because only newDir's level-1 sub-files were fetched from the DB
						//for (int i = 0; i < oldDir.SubFiles[oldSubFileIndex].SubFiles.Count; i++)
						foreach (RRFile subSubFile in oldSubFile.SubFiles)
						{
							newDir.SubFiles[newSubFileIndex].SubFiles.Add(subSubFile);
							subSubFile.SuperDir = newDir;
						}
						// advance to next old sub-node
						newSubFileIndex++;
						subNodeIndex++;
					}
					// append a new sub-node for each remaining new sub-dir
					for (; newSubFileIndex < newDir.SubFiles.Count; newSubFileIndex++)
					{
						// skip non-directory files
						if (!newDir.SubFiles[newSubFileIndex].IsDir)
						{
							continue;
						}
						TreeNode newSub = new TreeNode(newDir.SubFiles[newSubFileIndex].FileName + " (" + newDir.SubFiles[newSubFileIndex].FileID + ")",
														"" + subNodeIndex);
						newSub.PopulateOnDemand = true;
						node.ChildNodes.Add(newSub);
						// keep updating subNodeIndex to set new sub-node values
						subNodeIndex++;
					}
					//TODO??: remove extra subnodes that can be left when page is refreshed before it finishes loading
						// This seems to only happen if the page was recompiling when it was refreshed,
						// it doesn't seem to cause any inconsistency in the model,
						// and the view is easily fixed by another manual refresh (ie: not PostBack)
				}
				// if not rootFile, then remove old data from file tree; it either was replaced with new data or is no longer accessible
				if (oldDir.SuperDir != null)
				{
					oldDir.SuperDir.SubFiles.Remove(oldDir);	// remove old data
					oldDir.SuperDir = null;						// unlink oldDir from superDir
				}
				//TODO verify that oldDir has been completely decoupled and is ready to be garbage collected (ie: will not be a memory leak)
			}
			catch(Exception x)
			{
				// TODO alert user of something
			}
		}
////////
////////	Grid Event Handlers
////////
		// add onClick and onDblClick events to data rows, to allow selection and directory opening
		public void gridRowDataBound(object sender, GridViewRowEventArgs e)
		{
			GridViewRow r = e.Row;
			if (r.RowType == DataControlRowType.Header)// || r.RowType == DataControlRowType.EmptyDataRow)
			{
				// if root file is the selected dir, then don't show an up-one-dir link
				if (getFileForPath(selectedTreePath).SuperDir == null)
				{
					((GridView) sender).ShowHeader = false;
					return;
				}
				r.Cells[1].CssClass = "browsePermCol";
				r.Cells[2].CssClass = "browseNameCol truncatedText";
				r.Cells[3].CssClass = "browseModifiedCol";
				r.Cells[4].CssClass = "browseCreatedCol";
				// else: / * clear header and * / set onDblClick event to select parent dir
				/*
				for (int i = 0; i < r.Cells.Count; i++)
				{
					r.Cells[i].Text = i == 2 ? ".." : "";
				}*/
				r.Attributes.Add("ondblclick", Page.ClientScript.GetPostBackEventReference(fileBrowserTree, "s"+selectedTreePath.Substring(0, selectedTreePath.LastIndexOf('/')).Replace('/', '\\')));
				return;
			}
			if (r.RowType != DataControlRowType.DataRow)
			{
				return;
			}
			// on click, select row (js setTimeout function makes single-click code wait for 200ms to allow double-click to be processed instead)
			r.Attributes.Add("onclick", "setTimeout(\"" + Page.ClientScript.GetPostBackEventReference((GridView) sender, "Select$"+r.RowIndex) + "\", 200)");
			// if this file is a directory, then open dir on double-click
			RRFile f = (RRFile) r.DataItem;
			if (f.IsDir)
			{
				r.Attributes.Add("ondblclick", Page.ClientScript.GetPostBackEventReference(fileBrowserTree, "s"+selectedTreePath.Replace('/', '\\')+'\\'+getParentSubtypeIndexForFile(f)));
			}
		}
		// update context-sensitive interface controls
		public void gridRowSelected(object sender, EventArgs e)
		{
			// update session var
			selectedGridIndex = fileBrowserGrid.SelectedIndex;
			
			// cancel pending file actions
			CancelDeleteButton_Click(sender, e);
			if (SubmitFilePermissionsRequestButton.Visible)
			{
				CancelPermissionsRequestButton_Click(sender, e);
			}

			// update permissions-dependent interface controls
			RRFile f = getFileForPath(selectedFilePath);
			RRPermissionType p = f.EffectivePermission;

			downloadButton.Enabled = p >= RRPermissionType.Read && !f.IsDir;	// can't download a folder (TODO: yet)
			//copyToButton.Enabled = p >= RRPermissionType.Read;

			//renameButton.Enabled = p >= RRPermissionType.Write;

			deleteButton.Enabled = p == RRPermissionType.Admin && f.SuperDir.FileID != RRFile.EmptyRoot.FileID;	// can't directly delete project root dirs
			//moveToButton.Enabled = p == RRPermissionType.Admin;
			filePermButton.Enabled = p < RRPermissionType.Admin;
		}
////////
////////	Grid Functions
////////
		//TODO: sorting
////////
////////	Upload Event Handlers
////////
		public void SubmitUploadButton_Click(object sender, EventArgs e)
		{
			if (!FileUploadControl.HasFile)
			{
				UploadFileStatusLabel.Text = "No File was selected.";
				return;
			}
			string filename = UploadFileNewNameCheckBox.Checked ? UploadFileNameTextBox.Text : Path.GetFileName(FileUploadControl.FileName);
			UploadFileStatusLabel.Visible = true;
			if (!validateFilename(filename))
			{
				UploadFileNewNameCheckBox.Checked = true;
				OnCheckedNewFilename(sender, e);
				UploadFileStatusLabel.Text = "The current filename is not acceptable. The filename must be less than 40 characters and \\ / : * ? ' &quot &lt &gt | are not allowed.";
				return;
			}
			RRFile newFile = null;
			try
			{
				RRUser user = (RRUser) Session[GLOBALS.userObjSessionVar];
				int userID = user == null ? RRUser.GuestUserID : user.UserID;
				RRFile dir = RRDatabaseManager.getFileData(getFileForPath(selectedTreePath), userID);
				// check user permissions on parent directory
				if (dir == null || dir.EffectivePermission < RRPermissionType.Write)
				{
					UploadFileStatusLabel.Text = "Either you do not have write permissions in this folder or the folder no longer exists.";
					return;
				}
				// if filename already exists in dir according to DB
					// DB already handles filename uniqueness properly, but an error message from MySQL can be hard to parse for the user
				if (RRDatabaseManager.fileExists(dir.FileID, filename))
				{
					UploadFileNewNameCheckBox.Checked = true;
					OnCheckedNewFilename(sender, e);
					UploadFileStatusLabel.Text = "A file or folder with that name already exists. Please enter a new filname.";
					return;
				}
				// create new file and attach to superDir
					// only FileName, IsDir, CreatorID, Description, and SuperDirID are actually used by the createFile function;
					// all other properties are placeholders
				newFile = new RRFile(0, dir.FileID, filename, false, new MySqlDateTime(), userID, new MySqlDateTime(), userID, UploadFileDescriptionTextBox.Text,
											null, null, RRPermissionType.Admin, RRPermissionType.None, dir);
				//dir.SubFiles.Add(newFile);
				// insert file and user's admin permission into DB; on failure, excpetion is thrown and catch-block does cleanup
				RRDatabaseManager.createFile(newFile);
				RRDatabaseManager.createPermissions(userID, dir.FileID, filename, RRPermissionType.Admin);
				// upload file to server
				FileUploadControl.SaveAs(GLOBALS.repoDataRootDir + dir.FilePath.Replace('/', '\\') + filename);
				UploadFileStatusLabel.Text = "File uploaded!";
				// TODO: refresh file grid view on success
			}
			catch (Exception x)
			{
				// on file failure, remove new entry from DB and file tree
				if (newFile != null)
				{
					try
					{
						// deleting a file from DB cascades to also delete all associated permissions
						RRDatabaseManager.deleteFile(newFile);
					}
					catch (Exception x2)
					{
						//TODO recover/cleanup/report on DB cleanup fail??
					}
				}
				UploadFileStatusLabel.Text = "Server Error";//: " + x.Message;
			}
		}
		public void CancelUploadButton_Click(object sender, EventArgs e)
		{
			UploadForm.Visible = false;
		}
		protected void OnCheckedNewFilename(object sender, EventArgs e)
		{
			UploadFileNameTextBox.Enabled = UploadFileNewNameCheckBox.Checked;
			UploadFileNameRequiredValidator.Enabled = UploadFileNewNameCheckBox.Checked;
			// if NewFilename was just unchecked and new filename is empty, then autofill new filename with old filename
			if (UploadFileNameTextBox.Enabled && String.IsNullOrEmpty(UploadFileNameTextBox.Text))
			{
				UploadFileNameTextBox.Text = Path.GetFileName(FileUploadControl.FileName);
			}
		}
////////
////////	New Folder Event Handlers
////////
		public void SubmitNewFolderButton_Click(object sender, EventArgs e)
		{
			string dirName = NewFolderNameTextbox.Text;
			NewFolderStatusLabel.Visible = true;
			if (!validateFilename(dirName))
			{
				NewFolderStatusLabel.Text = "That folder name is not acceptable. Folder names must be less than 40 characters and \\ / : * ? ' &quot &lt &gt | are not allowed.";
				return;
			}
			RRFile newDir = null;
			try
			{
				RRUser user = (RRUser) Session[GLOBALS.userObjSessionVar];
				int userID = user == null ? RRUser.GuestUserID : user.UserID;
				RRFile superDir = RRDatabaseManager.getFileData(getFileForPath(selectedTreePath), userID);
				// check user permissions on parent directory
				if (superDir == null || superDir.EffectivePermission < RRPermissionType.Write)
				{
					NewFolderStatusLabel.Text = "Either you do not have write permissions in this folder or the folder no longer exists.";
					return;
				}
				// if filename already exists in dir according to DB
					// DB already handles filename uniqueness properly, but an error message from MySQL can be hard to parse for the user
				if (RRDatabaseManager.fileExists(superDir.FileID, dirName))
				{
					NewFolderStatusLabel.Text = "A file or folder with that name already exists. Please enter a new folder name.";
					return;
				}
				// create new file and attach to superDir
					// only FileName, IsDir, CreatorID, Description, and SuperDirID are actually used by the createFile function;
					// all other properties are placeholders
				newDir = new RRFile(0, superDir.FileID, dirName, true, new MySqlDateTime(), userID, new MySqlDateTime(), userID, NewFolderDescriptionTextBox.Text,
											null, null, RRPermissionType.Admin, RRPermissionType.None, superDir);
				// insert file and user's admin permission into DB; on failure, excpetion is thrown and catch-block does cleanup
				RRDatabaseManager.createFile(newDir);
				RRDatabaseManager.createPermissions(userID, superDir.FileID, dirName, RRPermissionType.Admin);
				// create dir on server
				Directory.CreateDirectory(GLOBALS.repoDataRootDir + superDir.FilePath.Replace('/', '\\') + dirName);
				NewFolderStatusLabel.Text = "New Folder Created!";
				// TODO: refresh file grid view on success
			}
			catch (Exception x)
			{
				// on file failure, remove new entry from DB and file tree
				if (newDir != null)
				{
					try
					{
						// deleting a file from DB cascades to also delete all associated permissions
						RRDatabaseManager.deleteFile(newDir);
					}
					catch (Exception x2)
					{
						//TODO recover/cleanup/report on DB cleanup fail??
					}
				}
				NewFolderStatusLabel.Text = "Server Error";//: " + x.Message;
			}
		}
		public void CancelNewFolderButton_Click(object sender, EventArgs e)
		{
			NewFolderForm.Visible = false;
		}
////////
////////	Delete File Event Handlers
////////
		public void ConfirmDeleteButton_Click(object sender, EventArgs e)
		{
			DeleteStatusLabel.Visible = true;
			RRFile oldFile = getFileForPath(selectedFilePath);
			try
			{
				RRUser user = (RRUser) Session[GLOBALS.userObjSessionVar];
				int userID = user == null ? RRUser.GuestUserID : user.UserID;
				RRFile newFile = RRDatabaseManager.getFileData(oldFile, userID);
				// check user permissions on file
				if (newFile == null || newFile.EffectivePermission < RRPermissionType.Admin)
				{
					DeleteStatusLabel.Text = "Either you do not have admin permissions on this " + (newFile.IsDir ? "Folder" : "File") + " or it no longer exists.";
					return;
				}
				newFile.SuperDir = oldFile.SuperDir;
				// delete file from DB; deleting a file from DB cascades to also delete all associated permissions
				RRDatabaseManager.deleteFile(newFile);
				// delete on server
				if (newFile.IsDir)
				{
					Directory.Delete(GLOBALS.repoDataRootDir + newFile.FilePath.Replace('/', '\\'), true);
				}
				else
				{
					File.Delete(GLOBALS.repoDataRootDir + newFile.FilePath.Replace('/', '\\'));
				}
				DeleteStatusLabel.Text = (newFile.IsDir ? "Folder" : "File") + " Deleted!";
				// TODO: refresh file grid view on success
			}
			catch (Exception x)
			{
				//TODO recover/cleanup/report on DB fail??
				DeleteStatusLabel.Text = "Server Error";//: " + x.Message;
			}
		}
		public void CancelDeleteButton_Click(object sender, EventArgs e)
		{
			DeleteConfirmation.Visible = false;
		}
////////
////////	Request Permissions Event Handlers
////////
		public void SubmitPermissionsRequestButton_Click(object sender, EventArgs e)
		{
			PermissionsRequestStatusLabel.Visible = true;
			if (!ReadPermissionButton.Checked && !WritePermissionButton.Checked && !AdminPermissionButton.Checked)
			{
				PermissionsRequestStatusLabel.Text = "Please select a permisstion type.";
				return;
			}
			bool isDirPerm = SubmitDirPermissionsRequestButton.Visible;
			try
			{
				RRUser user = (RRUser) Session[GLOBALS.userObjSessionVar];
				int userID = user == null ? RRUser.GuestUserID : user.UserID;
				RRFile file = isDirPerm ? getFileForPath(selectedTreePath) : getFileForPath(selectedFilePath);
				RRRequest req = new RRRequest();
				req.ReqType = RRRequestType.NewPerm;
				req.FileID = file.FileID;
				req.UserID = userID;
				// if file is not the repo root, then associate project with request to allow project admins to handle request
				if (file.FileID != RRFile.EmptyRoot.FileID)
				{
					req.ProjID = RRDatabaseManager.getProject(file).ProjId;
				}
				req.PermType = ReadPermissionButton.Checked ? RRPermissionType.Read : WritePermissionButton.Checked ? RRPermissionType.Write : RRPermissionType.Admin;
				req.Note = PermissionRequestNote.Text;
				// submit request to DB
				RRDatabaseManager.createRequest(req);
				PermissionsRequestStatusLabel.Text = "Request Submitted!";
				// TODO: refresh file grid view on success
			}
			catch (Exception x)
			{
				//TODO recover/cleanup/report on DB fail??
				PermissionsRequestStatusLabel.Text = "Server Error";//: " + x.Message;
			}
		}
		public void CancelPermissionsRequestButton_Click(object sender, EventArgs e)
		{
			PermissionsRequestForm.Visible = false;
		}
		public void UpdatePermissionRequestButtons(RRFile f)
		{
			ReadPermissionButton.Visible = f.EffectivePermission < RRPermissionType.Read;
			WritePermissionButton.Visible = f.EffectivePermission < RRPermissionType.Write;
		}
////////
////////	Util Functions
////////
		// get the file from the file tree model located at some path
			// the path is a string of the form "0/a/b/c/.../z", where a..z are ints and "/" can be any specified delimiter
			// note that every path starts with 0, for the root node
			// returns null if the file cannot be found
		public RRFile getFileForPath(string pathStr, string delimiter = "/")
		{
			string[] pathArr = pathStr.Split(delimiter.ToArray());
			int pathElem = int.Parse(pathArr[0]);
			// every path starts with 0 (root)
			if (pathElem != 0)
			{
				return null;
			}
			RRFile file = rootFile;
			for (int i = 1; i < pathArr.Length; i++)
			{
				pathElem = int.Parse(pathArr[i]);
				if (!file.IsDir || pathElem < 0 || pathElem >= file.SubFiles.Count)
				{
					return null;
				}
				file = file.SubFiles[int.Parse(pathArr[i])];
			}
			return file;
		}
		// get file subgroup index (either dir or file) within its parent's SubFiles list
			// ex: if f is the the 5th dir but the 10th element overall, then return 5
			// ex: if f is the the 7th file but the 12th element overall, then return 7
		public int getParentSubtypeIndexForFile(RRFile f)
		{
			if (f.SuperDir == null)
			{
				return -1;
			}
			int index = 0;
			List<RRFile> siblings = f.SuperDir.SubFiles;
			for(int i = 0; f.FileID != siblings[i].FileID; i++)
			{
				if (siblings[i].IsDir == f.IsDir)
				{
					index++;
				}
			}
			return index;
		}
		//
		protected bool validateFilename(string filename)
		{
			return System.Text.RegularExpressions.Regex.IsMatch(filename, "^[a-zA-Z0-9_\\-\\+\\=\\[\\]\\{\\} \\!\\@\\#\\$\\%\\^\\&\\(\\)\\~\\`\\,\\.\\;]{1,40}");
		}
	}
}